import copy
from colorama import Fore, init
init(autoreset=True)
import os, sys, traceback
# sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import re 
import shutil
from datetime import datetime as dt
import time
import threading
from natsort import natsorted

from hm.common import *
from hm.hos import *
from hm.htime import Str2Time
from hm.multi_process import MyPool

from report import HmTable, get_page_count_by_win32, BACKUP_DIR
from ODBC_report import logger, NotebookDocx, printf, FIX, CR


class ExecusStatistics:
    def __init__(self, executions):
        '''
            所有执行文件检查完后的，总结性的报告，可做为为Index，DTRs, SummaryOfChanges的父类
        '''
        self.executions = executions
        self.summaries = []
        for e in self.executions:
            self.summaries.append(e.s)
        logger.info(f"self.executions:{self.executions};self.summaries:{self.summaries}")
        self.get_execution_info()
        
    def get_execution_info(self):
        '''
        统计执行文件信息，比如总共有多少个SystemID, 用来检查Index文件等
        '''
        self.eSystemIDs, self.eSoftwareVersions, self.eTesters = [], [], []
        self.e_max_time = None
        for e in self.executions:
            for t in e.execution_tables:
                if t.SystemID not in self.eSystemIDs:
                    self.eSystemIDs.append(t.SystemID)
                for sw in t.SoftwareVersion.split("\n"): 
                    if sw and sw not in self.eSoftwareVersions:
                        self.eSoftwareVersions.append(sw)
                if t.Tester not in self.eTesters:
                    self.eTesters.append(t.Tester)
                if t._max_time:
                    if isinstance(t._max_time, str):
                        t._max_time = Str2Time(t._max_time).tt[0].dt
                    if self.e_max_time is None or (t._max_time and self.e_max_time<t._max_time):
                        self.e_max_time = t._max_time
                logger.info(f"""self.eSystemIDs:{self.eSystemIDs};
                    self.eSoftwareVersions:{self.eSoftwareVersions};
                    t.Tester:{t.Tester};
                    self.e_max_time:{self.e_max_time};
                """)
    
    def get_eDTRs(self, executions):
        '''
        return:
            DTR_data_original: [[#19999, ActualResult, [id1,..], [sw1,..], step_info_string], ...]
            case_edit_data: [[#序号, chapter, step, requirement, ActualResult, tester, time], ...]
        '''
        DTR_data_original = []
        DTR_data_table = []
        case_edit_data = []
        added_redmines = []
        added_edit = []
        edit_num = 0
        for e in executions:
            for t in e.execution_tables:
                for info in t.steps_info: # 当前步骤验证的需求
                    # 查找redmine
                    redmines = info['Redmines']
                    for redmine in redmines:
                        if redmine not in added_redmines:
                            added_redmines.append(redmine)
                            DTR_data_original.append([redmine, info["ActualResult"], [t.SystemID], [t.SoftwareVersion], {},])
                        else:
                            ix = added_redmines.index(redmine)
                            if t.SystemID not in DTR_data_original[ix][2]:
                                DTR_data_original[ix][2].append(t.SystemID)
                                DTR_data_original[ix][2].sort()
                            if t.SoftwareVersion not in DTR_data_original[ix][3]:
                                DTR_data_original[ix][3].append(t.SoftwareVersion)
                                DTR_data_original[ix][3].sort()
                        ix = added_redmines.index(redmine)
                        if e.case_name not in DTR_data_original[ix][4]:
                            DTR_data_original[ix][4][e.case_name] = {}
                        if t.SystemID not in DTR_data_original[ix][4][e.case_name]:
                            DTR_data_original[ix][4][e.case_name][t.SystemID] = []
                        DTR_data_original[ix][4][e.case_name][t.SystemID].append(info["step_num"])
                    # 查找test case edit
                    if info["TEST_CASE_EDIT"]:
                        m = (e.chapter, info['step_num']) 
                        if m not in added_edit:
                            added_edit.append(m)
                            r = '\n'.join(info['requirements'])
                            if not r:
                                r = "N/A"
                            # case_edit_data.append([str(edit_num+1)]+ list(m)+ [r, info["ActualResult"], t.Tester, t.EndDate]) # t._max_time
                            case_edit_data.append([str(edit_num+1)]+ list(m)+ [r, info, t.Tester, t.EndDate])
                            edit_num += 1
        # 生成redmine表格数据
        for d in DTR_data_original:
            info = ''
            for case_name in d[4]:
                info += case_name+":\n"
                for systemID in d[4][case_name]:
                    info += systemID+":\n"+"step "+str(d[4][case_name][systemID])+'\n'
                info += '\n'
            DTR_data_table.append([f"#{d[0]}", f"{d[1]}", f"INSTRUMENT J NUMBER:\n{', '.join(d[2])}\nSOFTWARE VERSION:\n{', '.join(d[3])}", info])
        return DTR_data_original, DTR_data_table, case_edit_data
                

class IndexHeaderTbl(HmTable):
    ATTR = {
        "platform":{"row":1, "col":1, 'regex':r' *Platform: *(.+?)\nGUI', "flags":re.I|re.DOTALL},
        "GUI":{"row":1, "col":1, 'regex':r'GUI: *((.(?<!MACO))+)', "flags":re.I|re.DOTALL},
        "date":{"row":0, "col":4},
        "compiler":{"row":1, "col":4}
    }
    def __init__(self, t, parent=None):
        '''
        '''
        super().__init__(t, parent)
        logger.info(f"********** [初始化Index Header表格] **********")
        for key in self.ATTR:
            logger.info(f"{key}:{getattr(self, key)};\n")
    
    def check(self):
        attr_d = [
            {"parent":natsorted(self.parent.eSystemIDs), "attr":'platform', "pre_val":"\n        ", "err":"缺少仪器"},
            {"parent":natsorted(self.parent.eSoftwareVersions), "attr":'GUI', "pre_val":"\n        ", "err":"缺少版本"},
            {"parent":natsorted(self.parent.eTesters), "attr":'compiler', "pre_val":"\n", "err":"缺少测试人员"},
        ]
        for d in attr_d:
            for p in d["parent"]:
                val = getattr(self, d['attr'])
                if not have_word(p, val):
                    if FIX:
                        val = val+d['pre_val']+p # 原来值+换行+新值
                        setattr(self, d['attr'], val)
                    yield CR.error(f"{d['err']}{p}", True)
        # for id_ in self.parent.eSystemIDs:
        #     if not have_word(id_, self.platform):
        #         e = f'缺少仪器{id_}'
        #         if FIX:
        #             self.platform = self.platform +f"\n        {id_}"
        #             e += FIX_success
        #         yield e
        
        # 检查时间
        __date = Str2Time(self.date).tt
        p_date = self.parent.e_max_time
        p_date_str = '' if not p_date else dt.strftime(p_date, "%d-%b-%Y")
        err = None
        if __date and p_date:
            if __date[0].dt < p_date:
                err = f'Date小于{p_date_str}'
        else:
            err = f'Date没写或写错，执行文件最大时间为{p_date_str}'
        if err:
            if FIX:
                self.date = p_date_str
            yield CR.error(err, True)


class IndexPageTbl(HmTable):
    def __init__(self, t, parent=None):
        '''
            页码表格
        '''
        super().__init__(t, parent)
        logger.info("********** [初始化Index Page表格] **********")
    
    @property
    def heads(self):
        '''
        因为操作表格会导致行索引改变，所以用property,但如果没必要，不要多次访问heads
        '''
        # todo 如果不是按照这个顺序会有问题
        heads = [
            {"name":"Test Summary",  "ins":self.parent.summaries, "description":"/"},
            {"name":"Test Execut", "ins":self.parent.executions, "description":"/"},
            {"name":"Objective Evidence", "description":"All testing evidence, including Images, videos and files."},
            {"name":"Summary of Changes", "ins":self.parent.sumChange, "description":"Summary Of Change Report"},
            {"name":"DTRs", "ins":self.parent.DTR, "description":"Defect Tracking Record"},
        ]
        for head in heads:
            head['location'] = self.locate_cell(r"\d+[^\w]+" + head["name"], col=0)
        logger.info(f"heads:\n{heads}")
        return heads
    
    @property
    def all_head_good(self):
        '''
            因为fix之后，此值可能改变，所以用property
        '''
        for d in self.heads:
            if not d["location"]: 
                return False
        return True 
    
    def check_report_heads(self):
        '''
            检查表格中的head(参见self.heads)行是否存在
        '''
        for i, d in enumerate(self.heads):
            if not d["location"]: 
                if FIX:
                    # 当前head在表格中不存在
                    # 查找后面的head中的第一个在表格中存在的行位置
                    end = -1
                    if i+1<len(self.heads):
                        for l in self.heads[i+1:]:
                            if l["location"]:
                                end = l["location"][0]
                                break
                    # 在找到的head前面插入新行，并写入当前不存在的head
                    row = self.insert_row(end)
                    self.set_content(f"{i+1}-{d['name']}", row.cells[0])
                    self.set_content(f"{d['description']}", row.cells[1])
                    # 更新self.heads
                    # self.update_heads()
                yield CR.error(f'在表格中找不到{d["name"]}，填写后继续...', True)
    
    def check_sum_exe_written_pages(self, head_ix):
        '''
            检查excution(或者summary，由head_ix决定)各个文件已写的页码，返回没写的和多余的页码
        '''
        # 用self.heads索引是因为self.heads可能有更新
        head = self.heads[head_ix]
        start, end = head["location"][0]+1, self.heads[head_ix+1]["location"][0] 
        not_found_ins = head['ins'][:]
        delete_rows = []
        for row in self.rows[start:end]:
            cells = row.cells
            description = cells[1].text.strip()
            page = cells[2].text.strip()
            # 检查页码格式
            if not page.isdigit():
                yield CR.error(f"{description}的页码不是数字")
                continue
            # 遍历exe或者smmary文件序列化器对象
            for p in head['ins']:
                if p is None:
                    continue
                name = p.case_name
                if not name:
                    name = OSname(p.filename)
                # exe或者smmary序列化器已填写
                if re.search(name+"$", description):
                    if p in not_found_ins:
                        not_found_ins.remove(p)
                    if str(p.PAGEs) != str(page):
                        if FIX:
                            self.set_content(str(p.PAGEs), cells[2])
                        yield CR.error(f"{description}页数{page}错误，应为{p.PAGEs}", True)
                    break
                # 当前行不对应任何exe或者smmary序列化器
                # 应该删除此行，但是删除会影响self.rows在这里的for循环，所以暂存到列表里，等for循环结束后再删除
                elif p==head['ins'][-1]:
                    if FIX:
                        delete_rows.append(row)
                    yield CR.error(f"{description}找不到对应文件", True)
        yield [not_found_ins, delete_rows]

    def delete_sum_exe_extra_rows(self, delete_rows):
        '''
        删除多余的页码
        '''
        if FIX:
            for row in delete_rows:
                self.delete_row(row)
            # 删除后self.rows会改变，所以要更新下面
            # self.update_heads()
            
    def check_sum_exe_pages(self):
        '''
            检查summary和execution页码
        '''
        if self.all_head_good:
            for i,d in enumerate(self.heads[:2]):
                # 检查sum和exe
                g = self.check_sum_exe_written_pages(i)
                for err in g:
                    if not isinstance(err, list):
                        yield err
                    else: 
                        not_found_ins, delete_rows = err
                        self.delete_sum_exe_extra_rows(delete_rows)
                        # 1.检查没写页码的用例
                        for j, p in enumerate(not_found_ins):
                            if p is None: 
                                continue
                            if FIX:
                                ix = self.heads[i]["location"][0]+j+1
                                row = self.insert_row(ix)
                                # 更新文档
                                self.set_content(f"{p.case_name}", row.cells[0])
                                self.set_content(f"{OSname(p.filename)}", row.cells[1])
                                self.set_content(f"{p.PAGEs}", row.cells[2])
                            yield CR.error(f"{p.filename}的页码为{p.PAGEs}，未填写", True)
                # 2.检查页码总数
                head = self.heads[i] 
                total_ix = head["location"][0]
                total_cell = self.rows[total_ix].cells[2]
                total_page_in_tbl = total_cell.text.strip()
                total_page_ins = sum([p.PAGEs for p in head["ins"] if p]) 
                if str(total_page_ins) != total_page_in_tbl:
                    if FIX:
                        self.set_content(f"{total_page_ins}", total_cell)
                    yield CR.error(f"{head['name']}总页码应为{total_page_ins}, 你写的是{total_page_in_tbl}", True)


    def check_sum_changes_DTRs(self):  
        if self.all_head_good:
            # 检查Summary Of Chang和DTR文件页码
            for head in self.heads[3:]:
                if head['ins']:
                    ins_page = head['ins'].PAGEs
                    row = head["location"][0]
                    cell = self.rows[row].cells[2] 
                    page = cell.text.strip()
                    if page!=str(ins_page):
                        if FIX:
                            self.set_content(f"{ins_page}", cell)
                        yield CR.error(f"{head['name']}页码{page}不正确，应该是{ins_page}", True)
                else:
                    yield(f"{head['name']}文件不存在，无法检查页码")

    def check(self):
        g_list = [
            self.check_report_heads(),
            self.check_sum_exe_pages(),
            self.check_sum_changes_DTRs(),
        ]
        for g in g_list:
            for err in g:
                yield err


class Index(NotebookDocx, ExecusStatistics):
    def __init__(self, path, *args, **kwargs):
        '''
            Index报告
        '''
        NotebookDocx.__init__(self, path)
        if self.is_me and args:
            self.setup(*args)
            
    def _is_me(self):
        try:
            cells = self.tables[0].rows[0].cells
            if have_word("Index", self.filename) and \
                have_word('Program/Project',cells[0].text):
                return True
        except:
            return False
        return False
        
    def setup(self, *args):
        '''
        因为要先__init__，判断is_me之后，再判断是否需要传入executions（检查所有的executions报告）, 所以创建了这个方法。
        '''
        ExecusStatistics.__init__(self, args[0])
        # todo 如果已经有这两个文件的notebookDocx对象，则直接使用已节省时间
        self.sumChange = NotebookDocx(self.summary_changes_path) if self.summary_changes_path else None
        self.DTR = NotebookDocx(self.DTR_path) if self.DTR_path else None
        self.head_tbl = IndexHeaderTbl(self.tables[0], self)
        # self.head_tbl.GUI = 'xxxx'
        # print(self.head_tbl.date, self.head_tbl.compiler)
        self.page_tbl = IndexPageTbl(self.tables[1], self)
    
    @classmethod
    def create_me(cls, executions):
        '''
            生成executions对应的Index文件（直接复制模板文件）
        '''
        index_dir = executions[0].index_dir
        if not index_dir:
            index_dir = os.path.join(OSparent(executions[0].path, 2), "0-Index")
            os.makedirs(index_dir, exist_ok=True)
        path = os.path.join(index_dir, "Index.docx")
        template = os.path.join(os.path.dirname(__file__),"res","Index-template.docx")
        shutil.copy2(template, path)
        doc = cls(path, executions)
        # doc.setup(executions)
        start = None
        for err in doc.check():
            if "😶" in err:
                printf(err)
            if "获取完成" in err:
                print("😶 正在生成，可能需要几分钟...", end = '')
                start = time.time()
            if start and time.time()-start>1:
                start = time.time()
                print(".", end="")
        
        doc.save(overwrite=True)
        return doc
    
    def refresh_pages(self, instance):
        instance.PAGEs = get_page_count_by_win32(instance.path)

    def log_pages(self):
        # 刷新PAGEs
        yield f"{Fore.LIGHTGREEN_EX}😶 获取页数信息中..."
        instances = [i for i in self.executions+self.summaries+[self.sumChange, self.DTR] if i is not None]
        
        # 多进程容易报错
        # pool = MyPool()
        # for instance in instances:
        #     pool.apply_async(get_page_count_by_win32, instance.path)
        # result = pool.read()
        # for i,ins in enumerate(instances):
        #     ins.PAGEs = result[i]
        #     yield f"{ins.filename}:{instance.PAGEs}"
        
        # 单线程
        num = 0
        for instance in instances:
            if instance:
                self.refresh_pages(instance)
                yield f"😶 {instance.filename}:{instance.PAGEs}"
                num+=1
        yield f"😶 获取完成, 共{len(instances)}个文件，{num}个已处理"
        # 多线程
        # ts = []
        # for instance in instances:
        #     if instance:
        #         # 可能比单线程快一点
        #         t_pages = threading.Thread(target=self.refresh_pages, args=(instance,))
        #         t_pages.setDaemon(True)
        #         t_pages.start()
        #         ts.append(t_pages)
        # for i,t in enumerate(ts):
        #     t.join()
        #     instance = instances[i]
        #     if instance:   
        #         yield f"{instance.filename}:{instance.PAGEs}"
        
        # yield f"获取页码时间: {time.time()-start}s"
        
        
    def check(self):
        # 1. 检查platform（所有设备）是否包含全部的execution
        # 2. 检查GUI（软件版本）是否包含全部的execution
        # 3. 检查Compiler是否包含全部的execution
        # 4. 检查日期是否大于全部execution
        # 5. 页码检查
        # 6. 自动生成index
        
        yield f"\n\n{Fore.LIGHTGREEN_EX}检查文件 {self.filename}"
        for err in super().check():
            yield err
        for err in self.head_tbl.check():
            yield f"    {err}"
        for err in self.log_pages():
            yield f"    {err}"
        yield f"正在检查页码，可能需要几分钟..."
        for err in self.page_tbl.check():
            yield f"    {err}"


class DTR(NotebookDocx, ExecusStatistics):
    def __init__(self, path, *args, **kwargs):
        '''

        '''
        NotebookDocx.__init__(self, path)
        if self.is_me and args:
            self.setup(*args)
        self.FIX = FIX
        
    
    def _is_me(self):
        try:
            cells = self.tables[0].rows[0].cells
            if have_word('DTR',cells[0].text) and \
                have_word('Description', cells[1].text):
                return True
        except:
            logger.warning(f"调用DTR is_me方法错误：{traceback.format_exc()}, 文件路径是{self.path}")
        return False
    
    def setup(self, *args):
        ExecusStatistics.__init__(self, args[0])
        self.redmine_tbl = HmTable(self.tables[0])
    
    @classmethod
    def create_me(cls, executions):
        '''
            生成executions对应的DTR文件（直接复制模板文件）
        '''
        DTR_dir = executions[0].DTR_dir
        if not DTR_dir:
            DTR_dir = os.path.join(OSparent(executions[0].path, 2), "5-DTRs")
            os.makedirs(DTR_dir, exist_ok=True)    
        filename = "Defect-Tracking-Record"
        path = os.path.join(DTR_dir, f"{filename}.docx")
        template = os.path.join(os.path.dirname(__file__),"res","Defect-Tracking-Record-template.docx")
        shutil.copy2(template, path)
        doc = cls(path, executions)
        doc.FIX = True
        for e in doc.check():
            pass
        # 删除第二行（空行）
        doc.redmine_tbl.delete_row(1)
        doc.save(overwrite=True)
        return doc

    def check_dtr_table(self, DTR_data):
        '''
            # todo not have the FIX currently
        '''
        # DTR_data: [[#19999, description, [id1,..], [sw1,..], step_info_string], ...]
        eDTRs = [i[0] for i in DTR_data] # exexetion DTR
        tDTRs = [] # table DTR
        for i, row in enumerate(self.redmine_tbl.rows[1:]):
            dtr_text = row.cells[0].text.strip() 
            instrument_sw_text = row.cells[2].text.strip()
            dtr_match = re.search(r'#?(\d{5})', dtr_text)
            if dtr_match is None:
                yield CR.error(f'第{i+1}行的redmine： {dtr_text}书写错误')
            else:
                dtr = dtr_match.group(1)
                if dtr not in eDTRs:
                    yield CR.error(f'第{i+1}行的redmine： {dtr_text}在execution报告中不存在')
                else:
                    data = DTR_data[eDTRs.index(dtr)]
                    IDs = data[2]
                    SWs = data[3]
                    for id in IDs+SWs:
                        if id not in instrument_sw_text:
                            yield CR.error(f'第{i+1}行的redmine： {dtr_text}缺少{id}')
                tDTRs.append(dtr)
        # 移除已写的DTR（剩下未写的）
        for i in tDTRs: 
            for j in DTR_data:
                if j[0] ==i:
                    DTR_data.remove(j)


    def check(self):
        yield f"\n\n{Fore.LIGHTGREEN_EX}检查文件 {self.filename}"
        for err in super().check():
            yield err
        DTR_data_original, DTR_data_table, case_edit_data = self.get_eDTRs(self.executions)
        for err in self.check_dtr_table(DTR_data_original):
            yield err
        for d in DTR_data_original:
            yield CR.error(f'未写DTR：{d[0]}', self.FIX)
            if self.FIX:
                row = self.redmine_tbl.insert_row(-1)
                dt = [i for i in DTR_data_table if d[0] in i[0]]
                dt = dt[0]
                # 精简描述信息
                des = re.search(r"\n?[^\n]+?"+dt[0][1:]+r"(.*?\n|.*$)", dt[1]) # 搜索包含redmine号所在的段落
                if des:
                    dt[1] = des.group().strip("\n")
                else:
                    dt[1] = ''
                for i in range(3):
                    self.redmine_tbl.set_content(dt[i], row.cells[i])


class SumOfChange(NotebookDocx, ExecusStatistics):
    def __init__(self, path, *args, **kwargs):
        '''
        '''
        NotebookDocx.__init__(self, path)
        if self.is_me and args:
            self.setup(*args)
        
    def _is_me(self):
        return True
    
    def setup(self, *args):
        ExecusStatistics.__init__(self, args[0])


    def check(self):
        '''
            没有检查方法，只有生成
        '''
        if FIX:
            executions = self.executions
            # 因为check之后，save的还是当前已打开的self.doc，而不是新创建的doc
            self.doc = SumOfChange.create_me(executions).doc
            yield f"{Fore.GREEN}我生成了新的Summary of Changes!"
        else:
            yield f"summary of changes 仅支持生成报告，不支持检查"
        
    
    @classmethod
    def create_me(cls, executions):
        DTR_dir = executions[0].summary_changes_dir
        if not DTR_dir:
            DTR_dir = os.path.join(OSparent(executions[0].path, 2), "4-Summary of Changes")
            os.makedirs(DTR_dir, exist_ok=True)    
        filename = "Summary Of Change Report-new.docx"
        path = os.path.join(DTR_dir, filename)
        template = os.path.join(os.path.dirname(__file__),"res","Summary Of Change Report-template.docx")
        shutil.copy2(template, path)
        
        doc = cls(path, executions)
        # doc.setup(executions)
        # for e in doc.check():
        #     pass
        # 填写内容
        # title: ['Field','Step#', 'Original Value', 'Changed Value', 'Justification for Change', 'User ID', 'Time of Change']
        # case_edit_data: [[#序号, chapter, step, requirement, ActualResult, tester, time], ...] 
        
        DTR_data_original, DTR_data_table, case_edit_data = doc.get_eDTRs(doc.executions)
        if case_edit_data:
            parg_init = doc.doc.paragraphs[1]
            tbl_init = doc.tables[0]
            tbl_cp = copy.deepcopy(tbl_init)
            # parg_init.text = case_edit_data[0][1] # this can't leave the old format
            parg_init.runs[0].text = case_edit_data[0][1]
            
            chapter = parg_init.text
            t = HmTable(tbl_init)
            for i,edit in enumerate(case_edit_data):
                # 精简描述信息
                ActualResult = edit[4]["ActualResult"]
                edit_content = re.search(r"Test case edit:*.{3,}?\n", ActualResult, flags=re.I|re.DOTALL) 
                if edit_content:
                    edit_content = edit_content.group().strip("\n")
                else:
                    edit_content = ''
                field, original_value = '', ''
                if re.search('action', ActualResult, flags=re.I):
                    field = "Action"
                    original_value = edit[4]['Action']
                elif re.search('expected result', ActualResult, flags=re.I):
                    field = "Expected Result"
                    original_value = edit[4]['ExpectedResult']
                    
                row_data = [field, edit[2], original_value, '', edit_content, edit[-2], edit[-1] ]
                if chapter!=edit[1]:
                    # new chapter
                    chapter=edit[1]
                    p = doc.doc.add_paragraph(chapter)
                    doc.cp_para_style(parg_init, p)
                    t = HmTable(doc.cp_table_to_para(tbl_cp, p))
                elif i!=0: 
                    t.insert_row(-1)
                cells =  t.rows[-1].cells
                for j, content in enumerate(row_data):
                    if not content:
                        content = ''
                    t.set_content(content, cells[j])

        doc.save(overwrite=True)
        return doc

